A. The Problem

Vendor lock-in and the need for a standard

Agenda

  • A. The Problem — Why every framework has its own tool format
  • B. MCP Architecture — The Model Context Protocol
  • C. Resources vs Tools — Active agency vs passive context
  • D. Wrap-up — MCP vs Custom + production strategy

The Fragmentation Problem

Every framework invented its own tool integration:

LangChain

@tool
def calculate(expr: str):
    """Evaluate math."""
    return eval(expr)

OpenAI

tools = [{
  "type": "function",
  "function": {
    "name": "calculate",
    ...
  }
}]

Semantic Kernel

@kernel_function
def calculate(expr: str):
    """Evaluate math."""
    return eval(expr)

The Problem

Write a tool once, rewrite it for every framework. No portability, no ecosystem.

The “USB” Analogy

🔌 Proprietary Cables

# 1. For OpenAI
openai_tool = {
  "type": "function",.
  "function": { ... }
}

# 2. For Claude
claude_tool = {
  "name": "my_tool",
  "input_schema": { ... }
}

# 3. For LangChain...
# 4. For Semantic Kernel...

✨ Universal Port (MCP)

# Define once...
@mcp.tool()
def my_tool(x: int) -> int:
    """Useful tool"""
    return x * 2

# Connects to EVERYONE:
# ✅ Claude Desktop
# ✅ IDEs (Cursor/Windsurf)
# ✅ Zed
# ✅ Any MCP Client

MCP = one standard protocol, any client can connect to any server.

MCP Architecture

G cluster_host MCP Host (Claude Desktop, IDE, Your App) cluster_server MCP Server (Your Code) C MCP Client T Tools @mcp.tool() C->T stdio / SSE R Resources @mcp.resource() C->R P Prompts @mcp.prompt() C->P

Component Role
Host The app where the AI lives (Claude, Cursor, your agent)
Client The connector inside the host that speaks MCP
Server Your code — exposes tools, resources, and prompts

B. MCP in Practice

Building with FastMCP

Setting Up a Server

FastMCP makes it as easy as FastAPI:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Research Assistant Tools")

@mcp.tool()
def calculate(operation: str, operand_a: float, operand_b: float) -> dict:
    """Executes a calculation using the internal registry."""
    return registry.execute("execute_calculation", {
        "operation": operation,
        "operand_a": operand_a,
        "operand_b": operand_b
    })

if __name__ == "__main__":
    mcp.run()  # Runs over stdio by default
  • @mcp.tool() — registers a function as an MCP tool
  • Type hints become the schema automatically
  • Docstrings become descriptions

Wrapping Your Existing Registry

The key insight: your ToolRegistry is the shared kernel. MCP is just one interface.

G R ToolRegistry Domain Logic MCP MCP Server @mcp.tool() R->MCP API REST API FastAPI R->API CLI CLI argparse R->CLI

MCP tools delegate to registry.execute() — all rate limiting, permissions, and error handling still apply.

Building an MCP Client

import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def run_agent():
    server_params = StdioServerParameters(
        command="python", args=["server.py"]
    )
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # Discover tools
            tools = await session.list_tools()

            # Call a tool
            result = await session.call_tool(
                "calculate",
                {"operation": "add", "operand_a": 10, "operand_b": 5}
            )
            print(result.content[0].text)

asyncio.run(run_agent())

The Stdio Transport

sequenceDiagram
    participant C as Client (simple_agent.py)
    participant S as Server (server.py)

    C->>S: spawn process (python server.py)
    C->>S: initialize request (via stdin)
    S-->>C: initialize response (via stdout)
    C->>S: list_tools request
    S-->>C: tools list response
    C->>S: call_tool("calculate", args)
    S-->>C: tool result

Stdio transport: client spawns server as a subprocess. They communicate via stdin/stdout pipes. No network, no ports — simple and secure for local use.

C. Resources vs Tools

Active agency vs passive context

Two Types of Capabilities

Tools Resources
Nature Active — do things Passive — read things
Examples Calculate, send email, query DB Logs, config files, data feeds
LLM interaction Model requests execution Model reads for context
Token cost Full tool-call cycle Direct context injection
Decorator @mcp.tool() @mcp.resource()

Resources in Practice

@mcp.resource("system://logs/recent")
def get_recent_logs() -> str:
    """Reads the last 10 lines of the application log."""
    try:
        with open("app.log", "r") as f:
            lines = f.readlines()
            return "".join(lines[-10:])
    except FileNotFoundError:
        return "No logs available."

The client can read system://logs/recent anytime — no tool call overhead.

Prompts: Saved Prompt Engineering

@mcp.prompt()
def review_code(code: str) -> str:
    """Standard code review prompt using our security guidelines."""
    return f"""Review the following code for:
    1. Hardcoded secrets
    2. SQL injection vulnerabilities
    3. Path traversal risks

    Code:
    {code}
    """

Prompts save best-practice prompt engineering into the server — users get consistent, high-quality interactions.

The Three Primitives

G cluster_mcp MCP Server C MCP Client T Tools Active Actions C->T call_tool R Resources Passive Data C->R read_resource P Prompts Saved Templates C->P get_prompt

D. Wrap-up

MCP vs Custom: When to Use Which

Feature Custom Registry (Session 2) MCP Server (Session 3)
Control Total — you own the loop Limited by the host/client
Interoperability Low — works only with your code High — any MCP client
Complexity Higher — build the chat loop Lower — just define functions
Best for Standalone products/apps Plugins, extensions, integrations

Production Strategy

Build your core logic in the Tool Registry (domain layer). Then expose it via multiple interfaces:

  1. MCP Interface — for IDEs, Claude Desktop, other AI clients
  2. API Interface — for your web frontend (React, etc.)
  3. CLI Interface — for devops and admin scripts
                    ┌─── MCP Server ──── Claude, Cursor, IDEs
                    │
  ToolRegistry ─────┼─── REST API ───── Web Frontend
  (shared kernel)   │
                    └─── CLI ─────────── Admin Scripts

Your registry.py is the shared kernel. MCP is just one view into it.

Up Next

Lab 4 — MCP Server: Wrap your registry in a FastMCP server, add a resource, and build a client that connects over stdio.